package org.persistente;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.persistente.banco.ConexaoBancoDados;
import org.persistente.banco.Conexoes;
import org.persistente.banco.FonteConexao;
import org.persistente.banco.GerenciadorConexoes;
import org.persistente.banco.Transacao;
import org.persistente.esquema.CampoAbstrato;
import org.persistente.esquema.CampoBoolean;
import org.persistente.esquema.CampoBytes;
import org.persistente.esquema.CampoCharacter;
import org.persistente.esquema.CampoDate;
import org.persistente.esquema.CampoDouble;
import org.persistente.esquema.CampoInteger;
import org.persistente.esquema.CampoLong;
import org.persistente.esquema.CampoString;
import org.persistente.esquema.CampoTimestamp;
import org.persistente.esquema.Tabela;

/**
 *
 * @author phireis
 */
@SuppressWarnings("unchecked")
public class Modelo<T extends Modelo<T>> {

    private static final Map<Class, Tabela> tabelas = new HashMap<Class, Tabela>();
    private static GerenciadorConexoes gerenciadorConexoes;
    private static Set<SqlListener> sqlListeners = new HashSet<SqlListener>();
    
    public static void addSqlListener(SqlListener listener){
        sqlListeners.add(listener);
    }
    public static void removeSqlListener(SqlListener listener){
        sqlListeners.remove(listener);
    }
    
    private static void notificaSql(final String sql){
        Thread th = new Thread(new Runnable() {

            public void run() {
                for(SqlListener lst : sqlListeners){
                lst.sqlExecutado(sql);
            }
            }
        });
        th.start();
    }
    
    


    public static GerenciadorConexoes setConexaoBancoDados(ConexaoBancoDados conexao){
        return gerenciadorConexoes = new GerenciadorConexoes(new FonteConexao(conexao));
    }

    public static void setGerenciadorConexoes(GerenciadorConexoes gerenciadorConexoes) {
        Modelo.gerenciadorConexoes = gerenciadorConexoes;
    }


    private static int getEstrategiaRetornoChaves(){
        return gerenciadorConexoes.getFonteConexao().getConexaoBancoDados().getEstrategiaRetornoChaves();
    }

    

    private static Tabela getTabela(Class classe){
        synchronized(tabelas){
            Tabela tabela = tabelas.get(classe);
            if(tabela == null){
                tabela = montaEsquema(classe);
                tabelas.put(classe, tabela);
                try {
                    Method m = classe.getMethod("esquema");
                    if(m != null){
                        m.invoke(null);
                    }    
                } catch(NoSuchMethodException e){
                    //...
                } catch (Exception ex) {
                    System.err.println("Erro ao buscar tabela: " + ex);
                }
                Connection con = gerenciadorConexoes.iniciaOperacao();
                try{
                    gerenciadorConexoes.getFonteConexao().getConexaoBancoDados().atualizaEsquema(tabela, con);
                }finally{
                    gerenciadorConexoes.terminaOperacao(con);
                }
            }
            return tabela;
        }
    }

    private static Tabela montaEsquema(Class classe) {

        String nome = classe.getSimpleName();
        StringBuilder sb = new StringBuilder();
        sb.append(Character.toLowerCase(nome.charAt(0)));
        for(int i = 1; i < nome.length(); ++i){
            char c = nome.charAt(i);
            if(Character.isUpperCase(c)){
                sb.append("_");
                sb.append(Character.toLowerCase(c));
            }
            else{
                sb.append(c);
            }
        }
        nome = sb.toString();

        Tabela tabela = new Tabela(nome);
        for(Field field : classe.getDeclaredFields()){
            try{
                if(Modifier.isStatic(field.getModifiers())){
                    continue;
                }
                if("id".equals(field.getName()) || "codigo".equals(field.getName())){
                    tabela.adicionaChave(create(field));
                }
                else{
                    tabela.adicionaCampo(create(field));
                }
            }catch(PersistenteException e){
                System.err.println("Atributo ignorado: " + field.getName());
            }
        }
        return tabela;
    }

    private static final Map<Class, Class<? extends CampoAbstrato>> tipos = new HashMap<Class, Class<? extends CampoAbstrato>>();
    static{
        tipos.put(Integer.class, CampoInteger.class);
        tipos.put(Long.class, CampoLong.class);
        tipos.put(Double.class, CampoDouble.class);
        tipos.put(String.class, CampoString.class);
        tipos.put(Character.class, CampoCharacter.class);
        tipos.put(Date.class, CampoDate.class);
        tipos.put(Timestamp.class, CampoTimestamp.class);
        tipos.put(byte[].class, CampoBytes.class);
        tipos.put(Boolean.class, CampoBoolean.class);
    }

    private static CampoAbstrato create(Field field){
        Class<? extends CampoAbstrato> classe = tipos.get(field.getType());
        if(classe == null){
            throw new PersistenteException("Tipo de atributo não suportado: " + field.getType().getName());
        }
        try {
            return classe.getConstructor(Field.class).newInstance(field);
        } catch (Exception ex) {
            throw new PersistenteException("Implementação de tipo de campo inválida.", ex);
        }
    }

    private Tabela tabela;

    private Tabela getTabela(){
        if(tabela == null){
            tabela = getTabela(this.getClass());
        }
        return tabela;
    }

    
    private static <T> int setParametros(PreparedStatement st, int pos, T entidade, Collection<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.get(entidade);
            ac.set(st, pos, val);
            ++pos;
        }
        return pos;
    }

    private static <T> int setParametrosNaoExatos(PreparedStatement st, int pos, T entidade, Collection<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.get(entidade);
            if(ac instanceof CampoString){
                val = "%" + val + "%";
            }
            ac.set(st, pos, val);
            ++pos;
        }
        return pos;
    }

    public static <T> void preencheEntidade(ResultSet rs, T entidade, Collection<CampoAbstrato> campos) throws SQLException {
        for (CampoAbstrato ac : campos) {
            Object val = ac.get(rs);
            ac.set(entidade, val);
        }
    }

    List<CampoAbstrato> getCampos(String[] atributos) {
        ArrayList<CampoAbstrato> campos = new ArrayList<CampoAbstrato>();
        for (String at : atributos) {
            campos.add(getTabela().getCampo(at));
        }
        return campos;
    }

    List<CampoAbstrato> getModificados() {
        ArrayList<CampoAbstrato> lista = new ArrayList<CampoAbstrato>();
        for (CampoAbstrato ac : getCampos()) {
            Object val = ac.get(this);
            if (val != null) {
                lista.add(ac);
            }
        }
        return lista;
    }


    public boolean estaModificado(){
        for(CampoAbstrato ac : getCampos()){
            Object val = ac.get(this);
            if(val != null){
                return true;
            }
        }
        return false;
    }

    public void reseta(){
        for(CampoAbstrato ac : getCampos()){
            ac.set(this, null);
        }
    }


    public static Connection getConexao(){
        return gerenciadorConexoes.iniciaOperacao();
    }

    private static void terminaOperacao(Connection con){
        gerenciadorConexoes.terminaOperacao(con);
    }

    public static void executa(Transacao transacao){
        Connection con = gerenciadorConexoes.abreTransacao();
        try{
            transacao.executa();
            con.commit();
        }catch(RuntimeException e){
            try {
                con.rollback();
            } catch (SQLException ex) {
                throw new PersistenteException("Rollback falhou.", ex);
            }
            throw e;
        }catch(Exception e){
            try {
                con.rollback();
            } catch (SQLException ex) {
                throw new PersistenteException("Rollback falhou.", ex);
            }
            throw new PersistenteException("Transação não completou.", e);
        }finally{
            gerenciadorConexoes.fechaTransacao();
        }
    }

    protected static void chavePrimaria(Class classe, String... chave){
        Tabela tabela = getTabela(classe);
        tabela.setChavePrimaria(chave);
    }



    private Set<CampoAbstrato> getChaves(){
        return getTabela().getChaves();
    }

    private Set<CampoAbstrato> getCampos(){
        return getTabela().getCampos();
    }

    private Set<CampoAbstrato> getOutros(){
        return getTabela().getOutros();
    }


    private boolean registroNovo = true;

    protected void tornaExistente(){
        registroNovo = false;
    }

    protected void tornaNovo(){
        registroNovo = true;
    }

    public boolean novo(){
        return registroNovo;
    }

    public void salva(){
        if(registroNovo){
            insereRetornando();
        }
        else{
            atualiza();
        }
    }

    public void insere() {
        valida();
        Connection con = null;
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> modificados = getModificados();
            String sql = GeradorSql.insere(getTabela().getNome(), modificados);

            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            st.executeUpdate();
            tornaExistente();
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(st);
            terminaOperacao(con);
        }

    }

    public void insereRetornando() {
        switch(getEstrategiaRetornoChaves()){
            case ConexaoBancoDados.RetornoChaves.RETURNING:{
                valida();
                Connection con = null;
                PreparedStatement st = null;
                ResultSet rs = null;
                try {
                    Collection<CampoAbstrato> modificados = getModificados();
                    Collection<CampoAbstrato> retorno = getChaves();
                    String sql = GeradorSql.insereRetornando(getTabela().getNome(), modificados, retorno);
                    notificaSql(sql);
                    con = getConexao();
                    st = con.prepareStatement(sql);
                    setParametros(st, 1, this, modificados);
                    rs = st.executeQuery();
                    rs.next();
                    preencheEntidade(rs, this, retorno);
                    tornaExistente();
                } catch (SQLException e) {
                    throw Conexoes.novaExcecao(e);
                } finally {
                    Conexoes.fecha(rs);
                    Conexoes.fecha(st);
                    terminaOperacao(con);
                }
                break;
            }
            case ConexaoBancoDados.RetornoChaves.JDBC:{
                Collection<CampoAbstrato> chaves = getChaves();
                if(chaves.size() != 1 || !(chaves.iterator().next() instanceof CampoInteger)){
                    insere();
                    return;
                    
                }

                valida();
                Connection con = null;
                PreparedStatement st = null;
                ResultSet rs = null;
                try {
                    Collection<CampoAbstrato> modificados = getModificados();
                    String sql = GeradorSql.insere(getTabela().getNome(), modificados);
                    notificaSql(sql);
                    con = getConexao();
                    st = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                    setParametros(st, 1, this, modificados);
                    st.executeUpdate();
                    rs = st.getGeneratedKeys();
                    if(rs.next()) {
                        int n = rs.getInt(1);
                        chaves.iterator().next().set(this, n);
                    }
                    tornaExistente();
                } catch(SQLException e) {
                    throw Conexoes.novaExcecao(e);
                } finally {
                    Conexoes.fecha(rs);
                    Conexoes.fecha(st);
                    terminaOperacao(con);
                }
                break;
            }

            default:{
                insere();
                return;
            }
        }

    }

    public void atualiza() {
        valida();
        Connection con = null;
        PreparedStatement st = null;
        try {
            Set<CampoAbstrato> naoChaves = getOutros();
            Set<CampoAbstrato> chaves = getChaves();
            String sql = GeradorSql.atualiza(getTabela().getNome(), naoChaves, chaves);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);

            int pos = setParametros(st, 1, this, naoChaves);
            setParametros(st, pos, this, chaves);
            st.executeUpdate();
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }
   
    public void exclui() {
        Connection con = null;
        PreparedStatement st = null;
        try {
            Set<CampoAbstrato> chaves = getChaves();
            String sql = GeradorSql.exclui(getTabela().getNome(), chaves);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, chaves);
            st.executeUpdate();
            tornaNovo();
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }


    public int excluiSemelhantes() {
        Connection con = null;
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> modificados = getModificados();
            String sql = GeradorSql.exclui(getTabela().getNome(), modificados);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    public T busca() {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> chaves = getChaves();
            Collection<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getTabela().getNome(), campos, chaves);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, chaves);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            preencheEntidade(rs, this, campos);
            tornaExistente();
            return (T)this;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } catch (Exception e) {
            throw new PersistenteException(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }
    
    public T buscaSemelhante() {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> modificados = getModificados();
            Collection<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getTabela().getNome(), campos, modificados);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            T nova = (T) getClass().newInstance();
            preencheEntidade(rs, nova, campos);
            nova.tornaExistente();
            return nova;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } catch (Exception e) {
            throw new PersistenteException(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    public T buscaParaAtualizar() {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> chaves = getChaves();
            Collection<CampoAbstrato> campos = getCampos();
            String sql = GeradorSql.pesquisa(getTabela().getNome(), campos, chaves) + " FOR UPDATE";
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, chaves);
            rs = st.executeQuery();
            if (!rs.next()) {
                return null;
            }
            preencheEntidade(rs, this, campos);
            tornaExistente();
            return (T)this;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } catch (Exception e) {
            throw new PersistenteException(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    public List<T> pesquisa() {
        return pesquisa(true);
    }
    
    public List<T> pesquisa(boolean exata) {
        return pesquisa(exata, new String[0], true);
    }

    public List<T> pesquisa(boolean exata, String[] ordem, boolean crescente) {
        return pesquisa(exata, ordem, crescente, "");
    }

    public List<T> pesquisa(boolean exata, String[] ordem, boolean crescente, String extra) {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        String sql = "";
        try {
            Collection<CampoAbstrato> campos = getCampos();
            Collection<CampoAbstrato> modificados = getModificados();
            sql = GeradorSql.pesquisa(getTabela().getNome(), campos, modificados, exata, getCampos(ordem), crescente, extra);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            if(exata){
                setParametros(st, 1, this, modificados);
            }
            else{
                setParametrosNaoExatos(st, 1, this, modificados);
            }
            rs = st.executeQuery();
            ArrayList<T> lista = new ArrayList<T>();
            while (rs.next()) {
                T nova = (T) getClass().newInstance();
                preencheEntidade(rs, nova, campos);
                nova.tornaExistente();
                lista.add(nova);
            }
            return lista;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } catch (Exception e) {
            throw new PersistenteException(e);
        } finally {
            Conexoes.fecha(st);
            Conexoes.fecha(rs);
            terminaOperacao(con);
        }
    }

    public int conta(){
        return conta(true);
    }

    public int conta(boolean exata) {
        return conta(exata, "");
    }

    public int conta(boolean exata, String extra) {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> modificados = getModificados();
            String sql = GeradorSql.seleciona(getTabela().getNome(), "COUNT(*)", modificados, exata);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql + " " + extra);

            setParametros(st, 1, this, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                throw new PersistenteException("Falha na comunicação com o banco de dados");
            }
            return rs.getInt(1);
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }


    ///SQLs perigosos:

    public List<T> pesquisaEspecifica(String filtroSql){
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> campos = getCampos();
            Collection<CampoAbstrato> modificados = getModificados();

            String sql = GeradorSql.seleciona(getTabela().getNome(), campos, filtroSql);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            rs = st.executeQuery();

            ArrayList<T> lista = new ArrayList<T>();
            while (rs.next()) {
                T nova = (T) getClass().newInstance();
                preencheEntidade(rs, nova, campos);
                nova.tornaExistente();
                lista.add(nova);
            }
            return lista;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } catch (Exception e) {
            throw new PersistenteException(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    public List<Object> selecionaColuna(String atributo){
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> modificados = getModificados();

            CampoAbstrato campo = getTabela().getCampo(atributo);
            if(campo == null){
                throw new PersistenteException("Atributo inválido: " + campo.getNome());
            }
            String retorno = campo.getNome();

            String sql = GeradorSql.seleciona(getTabela().getNome(), retorno, modificados, true);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            rs = st.executeQuery();
            ArrayList<Object> lista = new ArrayList<Object>();
            while(rs.next()){
                lista.add(campo.get(rs));
            }
            return lista;
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    public double calculoEspecifico(String retorno) {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            Collection<CampoAbstrato> modificados = getModificados();

            String sql = GeradorSql.seleciona(getTabela().getNome(), retorno, modificados, true);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);
            setParametros(st, 1, this, modificados);
            rs = st.executeQuery();
            if (!rs.next()) {
                throw new PersistenteException("Falha na comunicação com o banco de dados");
            }
            return rs.getDouble(1);
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(rs);
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }

    private int atualizaSemelhantes(T valores, List<CampoAbstrato> campos){
        Connection con = null;
        PreparedStatement st = null;
        try {
            List<CampoAbstrato> filtros = getModificados();
            List<CampoAbstrato> novos = campos;

            String sql = GeradorSql.atualiza(getTabela().getNome(), novos, filtros);
            notificaSql(sql);
            con = getConexao();
            st = con.prepareStatement(sql);

            int pos = setParametros(st, 1, valores, novos);
            setParametros(st, pos, this, filtros);
            return st.executeUpdate();
        } catch (SQLException e) {
            throw Conexoes.novaExcecao(e);
        } finally {
            Conexoes.fecha(st);
            terminaOperacao(con);
        }
    }


    public int atualizaSemelhantes(T valores, String... campos){
        return atualizaSemelhantes(valores, getCampos(campos));
    }

    public int atualizaSemelhantes(T valores){
        return atualizaSemelhantes(valores, valores.getModificados());
    }


    public void valida(){
        
    }


    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append(getTabela().getNome());
        for(CampoAbstrato campo : getCampos()){
            sb.append("  ");
            sb.append(campo.getNome());
            sb.append("=");
            sb.append(campo.get(this));
        }
        return sb.toString();
    }

    public String toXml(){
        StringBuilder sb = new StringBuilder();
        sb.append("<" + getTabela().getNome() + ">");
        for(CampoAbstrato campo : getCampos()){
            Object val = campo.get(this);
            if(val != null){
                sb.append("<" + campo.getNome() + ">");
                sb.append(val);
                sb.append("</" + campo.getNome() + ">");
            }
        }
        sb.append("</" + getTabela().getNome() + ">");
        return sb.toString();
    }

    @Override
    public T clone(){
        try {
            T novo = (T)getClass().newInstance();
            for(CampoAbstrato campo : getCampos()){
                campo.set(novo, campo.get(this));
            }
            if(!registroNovo){
                novo.tornaExistente();
            }
            return novo;
        } catch (Exception ex) {
            throw new PersistenteException(ex);
        }

    }
    
    public static <T> boolean equals(T ob1, T ob2){
        if(ob1 == null){
            return ob2 == null;
        }
        return ob1.equals(ob2);
    }



    @Override
    public boolean equals(Object outro){
        if(this == outro){
            return true;
        }
        if(getClass().isInstance(outro)){
            for(CampoAbstrato campo : getCampos()){
                if(!equals(campo.get(this), campo.get(outro))){
                    return false;
                }
            }
            return true;
        }
        return false;
        
    }

 
    @Override
    public int hashCode() {
        int hash = 7;
        for(CampoAbstrato campo : getChaves()){
            Object val = campo.get(this);
            hash = 37 * hash + (val != null ? val.hashCode() : 0);
        }
        return hash;
    }



  

}
